﻿// <copyright file="FieldOfView.cs" company="TenneySoftware">
// Copyright (c) 2009 All Right Reserved
// </copyright>
// <author>Robert Tenney</author>
// <email>rob10e@yahoo.com</email>
// <date>April 2009</date>
// <summary>Field of view algorithm using shadowcasting.</summary>

// License:

// Product: RPGRoguelikeEngine .Net
// Author: Robert N. Tenney
// Copyright: April 2009

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation version 2
// of the License.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

// This license covers all source code, binaries, and other materials related to this project
// unless specifically stated otherwise.

using System;
using System.Drawing;

namespace TenneySoftware.Engine {
    /// <summary>
    /// FOV is a class designed to find the areas in the dungeon that would lie within the field
    /// of view of the current position, using a shadowcasting algorithm.
    /// The algorithm use here was adapted from Bjorn Bergstrom.
    /// More information can be found at: 
    /// http://roguebasin.roguelikedevelopment.org/index.php?title=FOV_using_recursive_shadowcasting.
    /// </summary>
    public class FieldOfView {
        /// <summary>
        /// Level data to process.
        /// </summary>
        private Level data;
               
        /// <summary>
        /// Quadrant definition.
        /// </summary>
        private int[][] mult = new int[4][];

        /// <summary>
        /// Initializes a new instance of the FieldOfView class.
        /// </summary>
        /// <param name="data">Data layer of the dungeon.</param>
        public FieldOfView(Level data) {
            this.mult[0] = new int[8] { 1, 0, 0, -1, -1, 0, 0, 1 };
            this.mult[1] = new int[8] { 0, 1, -1, 0, 0, -1, 1, 0 };
            this.mult[2] = new int[8] { 0, 1, 1, 0, 0, -1, -1, 0 };
            this.mult[3] = new int[8] { 1, 0, 0, 1, -1, 0, 0, -1 };
            this.data = data;
        }

        /// <summary>
        /// Static method to run the FOV calculation algorithm.
        /// </summary>
        /// <param name="data">TileData grid.</param>
        /// <param name="playerLocation">Location of the player (Center of FOV).</param>
        /// <param name="range">The distance out from player to calculate for.</param>
        /// <returns>Returns true if successful.</returns>
        public static bool DoFieldOfViewCalculation(Level data, Point playerLocation, int range) {
            FieldOfView temp = new FieldOfView(data);
            temp.DoFieldOfViewCalculation(playerLocation.X, playerLocation.Y, range);
            return true;
        }
        
        /// <summary>
        /// Method to begin the FOV algorithm at the current location.
        /// </summary>
        /// <param name="x">X axis location.</param>
        /// <param name="y">Y axis location.</param>
        /// <param name="radius">Radius of FOV.</param>
        public void DoFieldOfViewCalculation(int x, int y, int radius) {
            // "Calculate lit squares from the given location and radius"
            for (int oct = 0; oct < 8; oct++) {
                this.TraceView(x, y, 1, 1.0, 0.0, radius, this.mult[0][oct], this.mult[1][oct], this.mult[2][oct], this.mult[3][oct]);
            }
        }

        /// <summary>
        /// Checks to see if the location will block the view.
        /// </summary>
        /// <param name="x">X location.</param>
        /// <param name="y">Y location.</param>
        /// <returns>True if the location blocks the view.</returns>
        private bool IsItBlocked(int x, int y) {
            if (!(x < 0 || y < 0 || x >= this.data.Width || y >= this.data.Height)) {
                return !this.data.GetLevel(x, y).Transparent;
            } else {
                return false;
            }
        }

        /// <summary>
        /// Sets the location to viewable.
        /// </summary>
        /// <param name="x">X location.</param>
        /// <param name="y">Y location.</param>
        private void SetCanSee(int x, int y) {
            if ((0 <= x  & x < this.data.Width) && (0 <= y & y < this.data.Height)) {
                this.data.GetLevel(x, y).CanSee = true;
                this.data.GetLevel(x, y).HasSeen = true;
            }
        }

        /// <summary>
        /// Scans the quadrant.
        /// </summary>
        /// <param name="cx">X location of center.</param>
        /// <param name="cy">Y location of center.</param>
        /// <param name="row">Row being scanned.</param>
        /// <param name="start">Start position of scan.</param>
        /// <param name="end">End position of scan.</param>
        /// <param name="radius">Distance from center for scanning.</param>
        /// <param name="xx">X offset X location.</param>
        /// <param name="xy">X offset Y location.</param>
        /// <param name="yx">Y offset X location.</param>
        /// <param name="yy">Y offset Y location.</param>
        private void TraceView(int cx, int cy, int row, double start, double end, int radius, int xx, int xy, int yx, int yy) {
            this.SetCanSee(cx, cy);
            int dx, dy;
            int x, y;
            double newStart = 0;
            double leftSlope, rightSlope;
            bool blocked;
            if (start < end) {
                return;
            }
            
            int radius_squared = radius * radius;
            for (int j = row; j <= radius + 1; j++) {
                dx = -j - 1;
                dy = -j;
                blocked = false;
                while (dx <= 0) {
                    dx += 1;
                    
                    // Translate the dx, dy coordinates into DataLayer coordinates:
                    x = cx + (dx * xx) + (dy * xy);
                    y = cy + (dx * yx) + (dy * yy);
                    
                    // l_slope and r_slope store the slopes of the left and right
                    // extremities of the square we're considering:
                    leftSlope = (dx - 0.5) / (dy + 0.5);
                    rightSlope = (dx + 0.5) / (dy - 0.5);
                    if (start < rightSlope) {
                        continue;
                    } else if (end > leftSlope) {
                        break;
                    } else {
                        // Our light beam is touching this square; light it:
                        if ((dx * dx) + (dy * dy) < radius_squared) {
                            this.SetCanSee(x, y);
                        }
                        
                        if (blocked) {
                            // we're scanning a row of blocked squares:
                            if (this.IsItBlocked(x, y)) {
                                newStart = rightSlope;
                                continue;
                            } else {
                                blocked = false;
                                start = newStart;
                            }
                        } else {
                            if (this.IsItBlocked(x, y) && j < radius) {
                                // This is a blocking square, start a child scan:
                                blocked = true;
                                this.TraceView(cx, cy, j + 1, start, leftSlope, radius, xx, xy, yx, yy);
                                newStart = rightSlope;
                            }
                        }
                    }
                }

                // Row is scanned; do next row unless last square was blocked:
                if (blocked) {
                    break;
                }
            }
        }
    }
}